package com.boom.springapp.javafxdemo.controller;

import com.boom.springapp.javafxdemo.common.Comment;
import com.boom.springapp.javafxdemo.common.CommentOrder;
import com.boom.springapp.javafxdemo.common.DefaultConfiguration;
import com.boom.springapp.javafxdemo.common.Stat;
import com.boom.springapp.javafxdemo.service.VideoCommentService;
import com.boom.springapp.javafxdemo.utils.BilibiliAPI;
import com.boom.springapp.javafxdemo.utils.CommentCsvSaver;
import com.boom.springapp.javafxdemo.utils.CommonTool;
import com.boom.springapp.javafxdemo.utils.GeoExporter;
import com.boom.springapp.javafxdemo.utils.GeoExporter.UnmatchedRegion;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.micrometer.common.util.StringUtils;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Window;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

/**
 * FXML控制器
 * 由Spring管理，实现依赖注入
 */
@Slf4j
@Component
public class VideoCommentController {
    @Autowired
    private VideoCommentService videoCommentService; // 注入Spring管理的服务

    public RadioButton timeSortRadio;
    public RadioButton likeSortRadio;
    public RadioButton replySortRadio;
    public CheckBox mapCheckBox;
    public Button stopButton;
    public ProgressBar downloadProgress;
    public Button getCommentButton;
    public Button clearLogButton;
    public Button generateMapButton;
    public Button generateWordCloudButton;
    public Button getImageButton;
    public TextArea logTextArea;
    public Label statusLabel;

    @FXML
    private ToggleGroup sortOrderGroup;
    @FXML
    private TextField BvEpSsInput;

    private Thread downloadThread;

    // 数据
    private String contentType;
    private String identifier;

    private boolean overwriteMode;

    private boolean stopFlag;

    private BilibiliAPI api;
    @Autowired
    private CommentCsvSaver commentCsvSaver;
    @Autowired
    private DefaultConfiguration defaultConfiguration;
    private ObjectMapper objectMapper = new ObjectMapper();

    private HashMap<String, String> CONTENT_TYPE_NAMES = new HashMap<String, String>() {
        {
            put("video", "视频");
            put("bangumi", "番剧剧集");
            put("season", "番剧季度");
        }
    };

    @FXML
    public void initialize() {
        api = new BilibiliAPI(defaultConfiguration.cookie, defaultConfiguration);
        // 初始化ToggleGroup，并设置默认选中项
        // TODO 需要读取默认配置
        sortOrderGroup.selectToggle(timeSortRadio);
        getCommentButton.setOnAction(event -> startDownloadComment());
    }

    /**
     * 获取评论按钮事件
     * 开始下载评论，最终保存为.csv文件。
     * 路径为默认配置中的路径，如果默认配置中没有配置路径，则在当前文件夹下创建默认文件夹，并设置到默认文件夹设置中。
     */
    private void startDownloadComment() {
        if (!validateInput()) {
            return;
        }
        if (downloadThread != null && downloadThread.isAlive()) {
            Alert alert = getAlert(Alert.AlertType.ERROR, "下载进行中", "请勿重复点击获取评论按钮");
            alert.showAndWait();
            return;
        }

        // 检查是否已存在相同标识符的数据
        String identifier = this.identifier;
        Path baseOutputDir = Paths
                .get(defaultConfiguration.defaultOutputPath != null ? defaultConfiguration.defaultOutputPath : "");
        // 查找以该标识符开头的目录
        List<Path> existingDirs = findExistingDirectories(baseOutputDir, identifier);

        // 检查是否存在数据
        boolean dataExists = false;
        List<String> existingFiles = new ArrayList<>();

        if (!existingDirs.isEmpty()) {
            for (Path dirPath : existingDirs) {
                Path csvFile = dirPath.resolve(identifier + ".csv");
                if (Files.exists(csvFile)) {
                    dataExists = true;
                    existingFiles.add(csvFile.toString());
                }
            }
        }

        // 如果数据已存在，询问用户是否覆盖
        if (dataExists) {
            try {
                String result = showOverwriteDialog(identifier, existingFiles);
                if ("cancel".equals(result)) {
                    log.info("用户取消了下载操作");
                    return;
                } else if ("overwrite".equals(result)) {
                    // 用户选择覆盖，设置覆盖标志
                    overwriteMode = true;
                    log.info("用户选择覆盖现有数据，将清空重新下载");
                } else {
                    return;
                }
            } catch (Exception e) {
                log.error("显示确认对话框时出错: {}", e.getMessage());
                getAlert(AlertType.ERROR, "错误", "无法显示确认对话框").showAndWait();
                return;
            }
        } else {
            overwriteMode = false;
        }
        // 初始化下载状态
        stopFlag = false;
        downloadProgress.setProgress(0.0);

        // 保存配置
        defaultConfiguration.defaultCommentOrder = sortOrderGroup.getSelectedToggle() == timeSortRadio
                ? CommentOrder.TIME.getOrder()
                : sortOrderGroup.getSelectedToggle() == likeSortRadio ? CommentOrder.LIKE.getOrder()
                : CommentOrder.REPLY.getOrder();
        defaultConfiguration.defaultGenerateMap = mapCheckBox.isSelected();

        // 更新API的cookie
        api = new BilibiliAPI(defaultConfiguration.cookie, defaultConfiguration);

        // 创建并启动线程
        // 创建并启动下载线程
        downloadThread = new Thread(() -> downloadComments());
        downloadThread.setDaemon(true);
        downloadThread.start();

    }

    /**
     * 显示覆盖确认对话框
     */
    private String showOverwriteDialog(String identifier, List<String> existingFiles) {
        // 在JavaFX中实现模态对话框
        Dialog<String> dialog = new Dialog<>();
        dialog.setTitle("数据已存在");
        dialog.setHeaderText("发现已存在 " + identifier + " 的数据");

        // 设置返回按钮
        ButtonType overwriteButtonType = new ButtonType("覆盖数据", ButtonBar.ButtonData.OK_DONE);
        ButtonType cancelButtonType = new ButtonType("取消操作", ButtonBar.ButtonData.CANCEL_CLOSE);
        dialog.getDialogPane().getButtonTypes().addAll(overwriteButtonType, cancelButtonType);

        // 创建对话框内容
        VBox content = new VBox(10);
        content.setPadding(new Insets(15));

        // 显示文件路径
        TextArea textArea = new TextArea();
        StringBuilder sb = new StringBuilder();
        sb.append("检测到以下位置已存在该视频的评论数据：\n\n");

        for (int i = 0; i < Math.min(existingFiles.size(), 2); i++) {
            Path path = Paths.get(existingFiles.get(i));
            sb.append("• ").append(path.getParent().getFileName())
                    .append("/").append(path.getFileName()).append("\n");
        }

        if (existingFiles.size() > 2) {
            sb.append("... 等共 ").append(existingFiles.size()).append(" 个文件");
        }

        textArea.setText(sb.toString());
        textArea.setEditable(false);
        textArea.setWrapText(true);

        content.getChildren().add(textArea);

        dialog.getDialogPane().setContent(content);

        // 处理结果
        dialog.setResultConverter(dialogButton -> {
            if (dialogButton == overwriteButtonType) {
                return "overwrite";
            } else if (dialogButton == cancelButtonType) {
                return "cancel";
            }
            return null;
        });

        Optional<String> result = dialog.showAndWait();
        return result.orElse("cancel");
    }

    /**
     * 查找baseOutputDir下以identifier2开头的目录
     *
     * @param baseOutputDir 基础目录路径
     * @param identifier2   目录名前缀
     * @return 匹配的目录路径列表
     */
    private List<Path> findExistingDirectories(Path baseOutputDir, String identifier2) {
        List<Path> result = new ArrayList<>();

        try {
            // 检查基础目录是否存在
            if (!Files.exists(baseOutputDir) || !Files.isDirectory(baseOutputDir)) {
                return result; // 返回空列表
            }

            // 遍历基础目录下的所有条目
            try (Stream<Path> entries = Files.list(baseOutputDir)) {
                entries.filter(path -> {
                    // 过滤条件：是目录且名称以identifier2开头
                    return Files.isDirectory(path) &&
                            path.getFileName().toString().startsWith(identifier2);
                }).forEach(result::add); // 将匹配的目录添加到结果列表中
            }
        } catch (IOException e) {
            log.error("查找目录时发生错误: {}", e.getMessage(), e);
        }

        return result;
    }

    private boolean validateInput() {
        String bvEpSsInputText = BvEpSsInput.getText();
        log.info("BV/EP/SS编号：{}", bvEpSsInputText);
        if (StringUtils.isEmpty(bvEpSsInputText)) {
            Alert alert = getAlert(Alert.AlertType.ERROR, "输入内容不能为空", "请输入有效的BV/EP/SS编号后再尝试获取评论。");
            alert.showAndWait();
            return false;
        }
        bvEpSsInputText = bvEpSsInputText.trim();
        try {
            if (bvEpSsInputText.startsWith("http")) {
                // 解析URL，获取内容类型和编号
                CommonTool.Pair<String, String> urlParts = CommonTool.parseBilibiliUrl(bvEpSsInputText);
                contentType = urlParts.getFirst();
                identifier = urlParts.getSecond();
                log.info("解析url:{} --> 内容类型：{}，编号：{}", bvEpSsInputText, contentType, identifier);
            } else {
                if (bvEpSsInputText.startsWith("BV")) {
                    Pattern pattern = Pattern.compile("^BV[a-zA-Z0-9]{10}$");
                    Matcher matcher = pattern.matcher(bvEpSsInputText);
                    if (matcher.matches()) {
                        contentType = "video";
                        identifier = bvEpSsInputText;
                        log.info("输入内容：{} --> 内容类型：{}，编号：{}", bvEpSsInputText, contentType, identifier);
                    } else {
                        Alert alert = getAlert(Alert.AlertType.ERROR, "输入内容格式错误", "BV号格式不正确，应为'BV'+10位字母数字组合");
                        alert.showAndWait();
                        return false;
                    }
                } else if (bvEpSsInputText.toLowerCase().startsWith("ep")) {
                    // EP号格式
                    String epId = bvEpSsInputText.toLowerCase().startsWith("ep") ? bvEpSsInputText.substring(2)
                            : bvEpSsInputText;
                    if (!epId.matches("\\d+")) {
                        Alert alert = getAlert(Alert.AlertType.ERROR, "输入内容格式错误", "EP号格式不正确，应为'EP'+数字");
                        alert.showAndWait();
                        return false;
                    }
                    contentType = "bangumi";
                    identifier = "EP" + epId;
                } else if (bvEpSsInputText.toLowerCase().startsWith("ss")) {
                    // SS号格式
                    String seasonId = bvEpSsInputText.toLowerCase().startsWith("ss") ? bvEpSsInputText.substring(2)
                            : bvEpSsInputText;
                    if (!seasonId.matches("\\d+")) {
                        Alert alert = getAlert(Alert.AlertType.ERROR, "输入内容格式错误", "SS号格式不正确，应为'SS'+数字");
                        alert.showAndWait();
                        return false;
                    }
                    contentType = "season";
                    identifier = "SS" + seasonId;
                } else {
                    Alert alert = getAlert(Alert.AlertType.ERROR, "输入格式不正确",
                            "请输入：\n• BV号（如：BV1xx411c7mD）\n• EP号（如：EP123456）\n• SS号（如：SS12345）\n• 完整链接");
                    alert.showAndWait();
                    return false;
                }
                log.info("输入内容：{} --> 内容类型：{}，编号：{}", bvEpSsInputText, contentType, identifier);
            }
        } catch (Exception e) {
            log.info("输入内容格式错误", e);
        }
        // 检查输出目录是否设置
        String outputDir = defaultConfiguration.defaultOutputPath != null ? defaultConfiguration.defaultOutputPath : "";
        log.info("输出目录：{}", outputDir);
        if (outputDir.isEmpty()) {
            Alert alert = getAlert(Alert.AlertType.ERROR, "输入格式不正确", "请先在设置中配置输出目录");
            alert.showAndWait();
            return false;
        }
        return true;
    }

    private Alert getAlert(Alert.AlertType alertType, String headerText, String contentText) {
        Alert alert = new Alert(alertType);
        alert.setTitle("输入验证");
        alert.setHeaderText(headerText);
        alert.setContentText(contentText);
        alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE);
        alert.getDialogPane().setMinWidth(400);
        Window window = getCommentButton.getScene().getWindow();
        alert.initOwner(window);
        alert.initModality(Modality.WINDOW_MODAL);
        return alert;
    }

    /**
     * 下载评论的线程函数 - 重写以支持不同内容类型
     */
    public void downloadComments() {
        try {
            String identifier = this.identifier;
            String contentType = this.contentType;

            log.info("开始获取{} {} 的评论", ("video".equals(contentType) ? "视频" : "番剧"), identifier);

            // 获取内容信息
            Map<String, Object> contentInfo = api.fetchContentInfo(identifier, contentType);

            if ((Integer) contentInfo.getOrDefault("code", -1) != 0) {
                String errorMsg = (String) contentInfo.getOrDefault("message", "未知错误");
                log.error("获取" + getContentTypeName(contentType) + "信息失败: " + errorMsg);
                return;
            }

            // 提取关键信息
            Object dataObj = contentInfo.getOrDefault("data", new HashMap<>());
            Map<String, Object> data = new HashMap<>();
            if (dataObj instanceof JsonNode) {
                // 如果是JsonNode，转换为Map
                JsonNode dataNode = (JsonNode) dataObj;
                try {
                    data = objectMapper.convertValue(dataNode, Map.class);
                } catch (Exception e) {
                    log.error("转换JsonNode到Map时出错: " + e.getMessage());
                    data = new HashMap<>();
                }
            } else if (dataObj instanceof Map) {
                // 如果已经是Map，直接转换
                data = (Map<String, Object>) dataObj;
            } else {
                data = new HashMap<>();
            }
            Object aidObj = data.get("aid");
            String contentTitle = (String) data.getOrDefault("title", "未知内容");

            if (aidObj == null) {
                log.info("无法获取有效的AID");
                return;
            }

            String aid = String.valueOf(aidObj);
            String oid = aid;

            log.info("获取到" + ("video".equals(contentType) ? "视频" : "番剧") + "信息: " + contentTitle);
            log.info("AID: " + aid);

            // 检查是否已经存在包含标题的目录
            String outputBaseDir = defaultConfiguration.defaultOutputPath;
            Path baseOutputDir = Paths.get(outputBaseDir);
            String videoTitle = contentTitle;
            Path existingDir = null;

            // 查找以标识符开头的目录
            try (DirectoryStream<Path> stream = Files.newDirectoryStream(baseOutputDir, identifier + "*")) {
                for (Path item : stream) {
                    if (Files.isDirectory(item)) {
                        existingDir = item;
                        String extractedTitle = extractTitleFromDirname(item.getFileName().toString());
                        if (extractedTitle != null && !extractedTitle.isEmpty()) {
                            videoTitle = extractedTitle;
                            log.info("找到已有目录，使用现有标题: " + videoTitle);
                            break;
                        }
                    }
                }
            } catch (IOException e) {
                log.error("遍历目录时出错: " + e.getMessage(), e);
            }

            // 如果没有找到现有标题，使用从API获取的标题
            if (existingDir == null || videoTitle.equals(contentTitle)) {
                videoTitle = contentTitle;
                log.info("使用API获取的标题: " + videoTitle);
            }

            // 创建输出目录 - 使用标识符+标题的格式
            String dirName = getIdentifierTitleDirName(identifier, videoTitle);
            Path outputDir = baseOutputDir.resolve(dirName);
            Files.createDirectories(outputDir);

            // 保存内容信息到JSON文件
            Path contentInfoPath = outputDir.resolve("content_info.json");
            if (!Files.exists(contentInfoPath) || overwriteMode) {
                try {
                    ObjectMapper mapper = new ObjectMapper();
                    mapper.writerWithDefaultPrettyPrinter().writeValue(contentInfoPath.toFile(), contentInfo);
                    log.info("已保存内容信息到: " + contentInfoPath);
                } catch (Exception e) {
                    log.error("保存内容信息失败: " + e.getMessage());
                }
            }

            log.info("开始获取" + ("video".equals(contentType) ? "视频" : "番剧") +
                    " " + identifier + " (" + videoTitle + ") 的评论");

            // 获取评论总数
            int total = api.fetchCommentCount(oid);
            if (total == 0) {
                log.error("未找到评论或获取评论数失败");
                return;
            }

            log.info("该" + getContentTypeName(contentType) + "共有 " + total + " 条评论");

            int downloadedCount = 0;
            int roundNum = 0;
            Set<String> recordedMap = new HashSet<>();
            Map<String, Stat> statMap = new HashMap<>();
            String offsetStr = "";

            // 从配置获取重试相关参数
            int maxRetries = defaultConfiguration.maxRetryCount;
            int consecutiveEmptyLimit = defaultConfiguration.emptyPageLimit;

            // 用于跟踪连续获取到的空页面数量
            int consecutiveEmptyPages = 0;

            // 判断三个按钮哪个被选中
            int corderVar = sortOrderGroup.getSelectedToggle() == timeSortRadio ? 0
                    : sortOrderGroup.getSelectedToggle() == likeSortRadio ? 1 : 2;
            List<Map<String, Object>> replyCollection = new ArrayList<>();
            while (!stopFlag) {

                log.info("正在获取第 " + (roundNum + 1) + " 页评论");

                // 如果已下载的评论数大于等于总评论数，且连续空页面数达到限制，则停止获取
                if (downloadedCount >= total && consecutiveEmptyPages >= consecutiveEmptyLimit) {
                    log.info(getContentTypeName(contentType) + " " + identifier + " (" + videoTitle + ") 的评论获取完成");
                    break;
                }

                roundNum++;
                int retryCount = 0;
                boolean success = false;
                List<Map<String, Object>> replies = new ArrayList<>();

                Map<String, Object> cmtInfo = new HashMap<>();
                // 请求评论并处理重试
                while (retryCount < maxRetries && !success && !stopFlag) {
                    cmtInfo = api.fetchComments(
                            oid, roundNum, corderVar, offsetStr);

                    // 检查API请求是否成功
                    if ((Integer) cmtInfo.getOrDefault("code", -1) != 0) {
                        String errorMsg = (String) cmtInfo.getOrDefault("message", "未知错误");
                        retryCount++;
                        if (retryCount < maxRetries) {
                            double retryDelay = defaultConfiguration.retryDelay;
                            log.info("请求评论失败: " + errorMsg + "，将在 " + retryDelay +
                                    " 秒后重试 (" + retryCount + "/" + maxRetries + ")...", "warning");
                            try {
                                Thread.sleep((long) (retryDelay * 1000));
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                            }
                            continue;
                        } else {
                            log.error("请求评论失败: " + errorMsg + "，已达到最大重试次数 " +
                                    maxRetries + "，跳过此页", "error");
                            break;
                        }
                    }

                    replies = (List<Map<String, Object>>) ((Map<String, Object>) cmtInfo
                            .getOrDefault("data", new HashMap<>()))
                            .getOrDefault("replies", new ArrayList<>());

                    // 处理空页面情况
                    if (CollectionUtils.isEmpty(replies)) {
                        consecutiveEmptyPages++;
                        retryCount++;

                        if (retryCount < maxRetries) {
                            double retryDelay = defaultConfiguration.retryDelay;
                            log.info("第 " + roundNum + " 页未获取到评论，连续空页面数: " +
                                    consecutiveEmptyPages + "，将在 " + retryDelay +
                                    " 秒后重试 (" + retryCount + "/" + maxRetries + ")...", "warning");
                            try {
                                Thread.sleep((long) (retryDelay * 1000));
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                            }
                            continue;
                        } else {
                            log.info("第 " + roundNum + " 页连续 " + consecutiveEmptyPages +
                                    " 次未获取到评论，已达到最大重试次数", "warning");
                            // 不设置success为True，让外层循环继续处理
                            break;
                        }
                    } else {
                        try {
                            success = true;
                            consecutiveEmptyPages = 0;
                            // 获取到了评论，重置连续空页面计数
                            replyCollection.addAll(replies);
                            if (cmtInfo != null && cmtInfo instanceof Map) {
                                Map<String, Object> cmtMap = (Map<String, Object>) cmtInfo;
                                Object dataObjTmp = cmtMap.get("data");
                                if (dataObjTmp instanceof ObjectNode) {
                                    ObjectNode dataNodeTmp = (ObjectNode) dataObjTmp;
                                    JsonNode cursorNode = dataNodeTmp.get("cursor");

                                    if (cursorNode instanceof ObjectNode) {
                                        ObjectNode paginationReplyNode = (ObjectNode) cursorNode.get("pagination_reply");
                                        if (paginationReplyNode != null) {
                                            Map<String, Object> paginationReply = objectMapper.convertValue(paginationReplyNode, Map.class);
                                            offsetStr = (String) paginationReply.getOrDefault("next_offset", "");
                                        }
                                    }
                                } else if (dataObjTmp instanceof Map) {
                                    Map<String, Object> dataMapTmp = (Map<String, Object>) dataObjTmp;
                                    Map<String, Object> cursorMapTmp = (Map<String, Object>) dataMapTmp.get("cursor");
                                    if (cursorMapTmp != null) {
                                        Map<String, Object> paginationReply = (Map<String, Object>) cursorMapTmp.get("pagination_reply");
                                        offsetStr = (String) paginationReply.getOrDefault("next_offset", "");
                                    }
                                }
                            }
                        } catch (Exception e) {
                            log.warn("解析分页信息时出错，使用默认偏移量: {}", e.getMessage());
                            offsetStr = "";
                        }
                    }
                }

                // 如果用户停止了下载或达到了连续空页面的限制，则跳出主循环
                if (stopFlag || (consecutiveEmptyPages >= consecutiveEmptyLimit && !success)) {
                    if (stopFlag) {
                        log.info("用户停止了下载");
                    } else {
                        log.info("连续 " + consecutiveEmptyPages + " 页未获取到评论，停止获取");
                    }
                    break;
                }

                // 如果请求失败且已重试达到上限，继续下一轮循环（尝试下一页）
                if (!success) {
                    continue;
                }

                // 获取子评论
                for (Map<String, Object> reply : replies) {
                    int rcount = (Integer) reply.getOrDefault("rcount", 0);
                    if (rcount == 0) {
                        continue;
                    }

                    List<Map<String, Object>> replyReplies = (List<Map<String, Object>>) reply.getOrDefault("replies",
                            new ArrayList<>());
                    if (!replyReplies.isEmpty() && replyReplies.size() == rcount) {
                        replyCollection.addAll(replyReplies);
                    } else {
                        // 需要额外获取子评论
                        List<Map<String, Object>> subReplies = fetchSubComments(oid,
                                String.valueOf(reply.get("rpid")), identifier);
                        replyCollection.addAll(subReplies);
                    }
                }

                // 处理置顶评论
                List<Map<String, Object>> topReplies = (List<Map<String, Object>>) ((Map<String, Object>) cmtInfo
                        .getOrDefault("data", new HashMap<>()))
                        .getOrDefault("top_replies", new ArrayList<>());
                if (!topReplies.isEmpty()) {
                    replyCollection.addAll(topReplies);
                    for (Map<String, Object> reply : topReplies) {
                        List<Map<String, Object>> replyReplies = (List<Map<String, Object>>) reply
                                .getOrDefault("replies", new ArrayList<>());
                        if (!replyReplies.isEmpty()) {
                            replyCollection.addAll(replyReplies);
                        }
                    }
                }

                // 转换为Comment对象
                List<Comment> comments = new ArrayList<>();
                for (Map<String, Object> reply : replyCollection) {
                    String rpid = String.valueOf(reply.get("rpid"));
                    if (!recordedMap.contains(rpid)) {
                        Comment comment = Comment.fromApiResponse(reply);
                        comment.setBvid(identifier); // 使用统一的标识符

                        recordedMap.add(rpid);
                        comments.add(comment);

                        // 统计地区信息
                        if (mapCheckBox.isSelected()) {
                            String location = comment.getLocation();
                            if (location == null || location.isEmpty()) {
                                location = "未知";
                            }

                            // 规范化地区名称，与CSV分析保持一致
                            String normalizedLocation = normalizeLocation(location);

                            // 确保用户ID是字符串类型
                            String userId = String.valueOf(comment.getMid());

                            if (statMap.containsKey(normalizedLocation)) {
                                Stat stat = statMap.get(normalizedLocation);
                                stat.setLocation(stat.getLocation() + 1); // 评论数增加
                                stat.setLike(stat.getLike() + comment.getLike());
                                stat.getLevel()[comment.getCurrentLevel()] += 1;
                                stat.getUsers().add(userId); // 添加用户ID到集合
                                stat.updateUserSex(userId, comment.getSex()); // 更新用户性别统计
                            } else {
                                Stat stat = new Stat(normalizedLocation, 1, comment.getLike());
                                stat.getLevel()[comment.getCurrentLevel()] += 1;
                                stat.getUsers().add(userId); // 添加用户ID到集合
                                stat.updateUserSex(userId, comment.getSex()); // 更新用户性别统计
                                statMap.put(normalizedLocation, stat);
                            }
                        }
                    }
                }

                // 保存到CSV
                if (!comments.isEmpty()) {
                    boolean overwriteMode = this.overwriteMode;
                    commentCsvSaver.saveToCsv(identifier, comments, outputDir.toString(), videoTitle, overwriteMode);
                    // 如果是覆盖模式，只在第一次调用时覆盖，后续调用应该追加
                    if (overwriteMode) {
                        this.overwriteMode = false; // 重置覆盖标志，后续为追加模式
                    }
                }

                downloadedCount += comments.size();
                log.info("已获取 " + downloadedCount + "/" + total + " 条评论");

                // 更新进度条
                downloadProgress.setProgress(Math.min(100, (double) downloadedCount / total * 100));

            }

            // 生成地图
            if (mapCheckBox.isSelected() && !statMap.isEmpty()) {
                log.info("统计到 " + statMap.size() + " 个地区的数据");
                for (Map.Entry<String, Stat> entry : statMap.entrySet()) {
                    log.info("  " + entry.getKey() + ": " + entry.getValue().getLocation() + " 条评论");
                }

                log.info("正在生成评论地区分布地图...");
                Map<String, UnmatchedRegion> unmatchedRegions = GeoExporter.writeGeoJson(
                        statMap, identifier, outputDir.toString(), videoTitle);
                log.info("地图生成完成");

                // 显示未匹配地区
                if (unmatchedRegions != null && !unmatchedRegions.isEmpty()) {
                    String unmatchedNames = String.join(", ", unmatchedRegions.keySet());
                    log.info("有 " + unmatchedRegions.size() + " 个地区未能匹配到地图: " + unmatchedNames);

                    // 单独打印每个未匹配地区的信息
                    for (Map.Entry<String, UnmatchedRegion> entry : unmatchedRegions.entrySet()) {
                        String region = entry.getKey();
                        UnmatchedRegion count = entry.getValue();
                        log.info("  未匹配地区: " + region + " - " + count.getComments() +
                                "条评论, " + count.getUsers() + "位用户");
                    }
                }
            }

            log.info("任务完成");
            downloadProgress.setProgress(100);

        } catch (Exception e) {
            log.info("下载过程中出错: " + e.getMessage());
            log.error("下载评论出错", e);
        }
    }

    private String normalizeLocation(String location) {
        // 扩展点？
        return location;
    }

    /**
     * 获取子评论 - 更新以使用统一标识符
     *
     * @param oid        评论区ID
     * @param rpid       父评论ID
     * @param identifier 内容标识符
     * @return 子评论列表
     */
    public List<Map<String, Object>> fetchSubComments(String oid, String rpid, String identifier) {
        int page = 1;
        List<Map<String, Object>> allReplies = new ArrayList<>();

        try {
            while (!stopFlag) {
                log.info("获取评论 " + rpid + " 的子评论，第 " + page + " 页");

                // 构建请求参数
                Map<String, String> params = new HashMap<>();
                params.put("oid", oid);
                params.put("type", "1");
                params.put("root", rpid);
                params.put("ps", "20");
                params.put("pn", String.valueOf(page));

                try {
                    // 构建URL
//                    StringBuilder urlBuilder = new StringBuilder("https://api.bilibili.com/x/v2/reply/reply?");
//                    boolean first = true;
//                    for (Map.Entry<String, String> entry : params.entrySet()) {
//                        if (!first) {
//                            urlBuilder.append("&");
//                        }
//                        urlBuilder.append(entry.getKey()).append("=").append(entry.getValue());
//                        first = false;
//                    }
//                    String url = urlBuilder.toString();
//
//                    // 设置请求头
//                    Map<String, String> headers = new HashMap<>();
//                    headers.put("User-Agent",
//                            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0");
//                    headers.put("Referer", "https://www.bilibili.com/" +
//                            ("video".equals(contentType) ? "video" : "bangumi/play") + "/" + identifier);
//                    headers.put("Origin", "https://www.bilibili.com");
//                    headers.put("Accept", "application/json, text/plain, */*");
//
//                    if (api.getCookie() != null && !api.getCookie().isEmpty()) {
//                        headers.put("Cookie", api.getCookie());
//                    }
//
//                    // 请求前添加延迟
//                    api.sleepBetweenRequests("normal");
//
//                    // 发送请求
//                    HttpGet request = new HttpGet(url);
//                    for (Map.Entry<String, String> header : headers.entrySet()) {
//                        request.setHeader(header.getKey(), header.getValue());
//                    }
//
//                    CloseableHttpResponse response = api.getHttpClient().execute(request);
                    Map<String, Object> data = api.fetchSubComments(oid, Long.parseLong(rpid), page);

                    if ((Integer) data.getOrDefault("code", -1) != 0) {
                        log.info("获取子评论失败: " + data.getOrDefault("message", "未知错误"));
                        break;
                    }
                    List<Map<String, Object>> replies = (List<Map<String, Object>>) ((Map<String, Object>) data
                            .getOrDefault("data", new HashMap<>()))
                            .getOrDefault("replies", new ArrayList<>());

                    if (CollectionUtils.isEmpty(replies)) {
                        break;
                    }

                    allReplies.addAll(replies);
                    page++;
                } catch (Exception e) {
                    log.info("获取子评论出错: " + e.getMessage());
                    // 请求失败添加重试延迟
                    api.sleepBetweenRequests("retry");
                    break;
                }
            }
        } catch (Exception e) {
            log.info("子评论处理过程中出错: " + e.getMessage());
        }

        return allReplies;
    }

    /**
     * 生成标识符和视频标题组合的目录名称
     * 该方法将标识符与经过安全处理的视频标题组合成一个适合用作目录名的字符串
     *
     * @param identifier2 视频标识符（如BV号）
     * @param videoTitle  视频标题
     * @return 组合后的目录名称，格式为"标识符_安全处理后的视频标题"
     */
    private String getIdentifierTitleDirName(String identifier2, String videoTitle) {
        // 对视频标题进行安全处理，移除或替换不适合用作文件名的字符
        String safeTitle = sanitizeFilename(videoTitle);
        // 返回组合后的目录名称：标识符 + 下划线 + 安全处理后的标题
        return identifier2 + "_" + safeTitle;
    }

    /**
     * 清理文件名
     *
     * @param filename 原始文件名
     * @return 清理后的文件名
     */
    public static String sanitizeFilename(String filename) {
        if (filename == null) {
            return null;
        }

        // 替换文件名中不允许的字符
        String invalidChars = "<>:\"/\\|?*";
        for (char ch : invalidChars.toCharArray()) {
            filename = filename.replace(ch, '_');
        }

        // 截断文件名长度，防止过长
        if (filename.length() > 100) {
            filename = filename.substring(0, 97) + "...";
        }

        return filename;
    }

    /**
     * 从目录名称中提取视频标题
     * <p>
     * 格式: BV号_标题 或 EP号_标题
     *
     * @param dirname 目录名称
     * @return 提取出的标题，如果无法提取则返回空字符串
     */
    public static String extractTitleFromDirname(String dirname) {
        if (dirname == null || dirname.isEmpty()) {
            return "";
        }

        String[] parts = dirname.split("_", 2);
        if (parts.length > 1 && (parts[0].startsWith("BV") || parts[0].startsWith("EP"))) {
            return parts[1];
        }
        return "";
    }

    /**
     * 获取类型名称
     *
     * @param contentType2
     * @return
     */
    private String getContentTypeName(String contentType2) {
        return CONTENT_TYPE_NAMES.getOrDefault(contentType2, "unknown");
    }

}
